"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createServer = void 0;
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const chokidar_1 = __importDefault(require("chokidar"));
const fastify_1 = __importDefault(require("fastify"));
const chalk_1 = __importDefault(require("chalk"));
const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
const fastify_http_proxy_1 = __importDefault(require("fastify-http-proxy"));
const fastify_static_1 = __importDefault(require("fastify-static"));
const magic_string_1 = __importDefault(require("magic-string"));
const es_module_lexer_1 = require("es-module-lexer");
const plugins_1 = require("./plugins");
const utils_1 = require("./utils");
// constrant variable
const fsPromises = fs_1.default.promises;
const __ES_PATH__ = '/tserve/eventsource';
function createServer(options) {
    const server = fastify_1.default({
        logger: process.env.NODE_ENV === 'development'
    });
    // start watcher
    const watcher = chokidar_1.default.watch(options.root, {
        ignored: [
            '.git',
            'node_modules',
        ],
        cwd: process.cwd()
    });
    server.decorate('watcher', watcher);
    // set static server
    server.register(fastify_static_1.default, {
        root: options.root
    });
    if (options.proxy) {
        for (const item of options.proxy) {
            server.register(fastify_http_proxy_1.default, item);
        }
    }
    // local plugin
    server.register(eventSourcePlugin, options);
    server.register(resolveModule, options);
    server.register(resolveJs, options);
    server.register(plugins_1.resolveCss, options);
    server.register(plugins_1.resolveVue, options);
    server.register(serverIndex, options);
    // handle error
    server.addHook('onError', (request, reply, error) => __awaiter(this, void 0, void 0, function* () {
        console.error(chalk_1.default.red(error), `\nurl: ${request.url}`);
        reply.code(500);
        reply.send('<h1>Sorry, tserve encounter an unhandled error</h1>');
        reply.header('Content-Type', 'text/html');
        return;
    }));
    watcher.on('error', (error) => {
        console.error(`[tserve watcher]`, error);
    });
    watcher.on('close', () => {
        console.log('watcher is closed.');
    });
    return server;
}
exports.createServer = createServer;
// resolve module for module
const resolveModule = fastify_plugin_1.default((instance, options) => __awaiter(void 0, void 0, void 0, function* () {
    // init lexer
    yield es_module_lexer_1.init;
    // find module
    const nodeModulePath = path_1.default.resolve(options.root, 'node_modules');
    // define file to module map, and for some special module to define manual 
    const nodeModules = {
        vue: 'node_modules/vue/dist/vue.esm.browser.js'
    }; // module_name => path
    // read node_modules
    const promises = [];
    let stack = [
        ...yield fsPromises.readdir(nodeModulePath)
    ];
    let currentDir = '.';
    console.log('loading node_modules...');
    while (stack.length) {
        const dir = stack.pop();
        const dirKey = path_1.default.join(currentDir, dir);
        if (dir === '.bin')
            continue;
        if (dir === 'tserve')
            continue;
        if (dir === '@types')
            continue;
        if (Object.keys(nodeModules).indexOf(dirKey) > -1)
            continue;
        // @ means this dir is an scope
        if (dir.startsWith('@')) {
            if (dir === currentDir) {
                // child dir traversal finish.
                if (~currentDir.indexOf('/')) {
                    currentDir = currentDir.substring(0, currentDir.lastIndexOf('/'));
                }
                else {
                    currentDir = '.';
                }
                continue;
            }
            if (currentDir === '.') {
                currentDir = dir;
            }
            else {
                currentDir = path_1.default.join(dir, currentDir);
            }
            stack.push(currentDir);
            stack.push(...yield fsPromises.readdir(path_1.default.join(nodeModulePath, dir)));
        }
        else {
            promises.push(fsPromises.readFile(path_1.default.resolve(nodeModulePath, dirKey, 'package.json'), { encoding: 'utf-8' })
                .then(content => {
                const json = JSON.parse(content);
                if (json.main)
                    nodeModules[dir] = path_1.default.join('node_modules', dirKey, json.main);
                else
                    nodeModules[dir] = path_1.default.join('node_modules', dirKey, 'index.js');
                console.log(`loaded module: ${dirKey}`);
            }).catch(e => {
                console.error(`error: ${e}\n, on read module ${dirKey}`);
            }));
        }
    }
    yield Promise.all(promises);
    console.log('load finish');
    // decorate nodeModules and other plugin could use nodeModules
    instance.decorate('nodeModules', nodeModules);
    // to replace import of source
    instance.decorate('rewriteImport', (source) => {
        const imports = es_module_lexer_1.parse(source)[0];
        const fileMagicString = new magic_string_1.default(source);
        if (imports.length) {
            for (const item of imports) {
                const _moduleName = source.substring(item.s, item.e);
                // if _moduleName is dependencies, rewrite it to path width  node_modules
                if (nodeModules[_moduleName]) {
                    console.debug(`replace _moduleName:${_moduleName} to ${nodeModules[_moduleName]}`);
                    fileMagicString.overwrite(item.s, item.e, '/' + nodeModules[_moduleName]);
                }
                else {
                    // if imoprt css module
                    if (utils_1.isCss.test(_moduleName)) {
                        console.debug(`replace css module: ${_moduleName} to ${_moduleName}?import`);
                        fileMagicString.overwrite(item.s, item.e, `${_moduleName}?import`);
                    }
                }
            }
        }
        return fileMagicString.toString();
    });
}));
const resolveJs = fastify_plugin_1.default((instance, options) => __awaiter(void 0, void 0, void 0, function* () {
    // cached file
    const cachedFileMap = {};
    instance.addHook('preHandler', (request, reply) => __awaiter(void 0, void 0, void 0, function* () {
        if (request.url.startsWith('/tserve'))
            return;
        if (request.url.startsWith('/node_modules'))
            return;
        const url = request.url.split('?')[0];
        if (url.endsWith('.js')) {
            if (cachedFileMap[url]) {
                reply.type('application/javascript');
                return reply.send(cachedFileMap[url]);
            }
            try {
                const fileContent = yield fsPromises.readFile(url.substring(1), { encoding: 'utf-8' });
                const result = instance.rewriteImport(fileContent);
                cachedFileMap[url] = result;
                console.debug(`sending file ${request.url}`);
                reply.header('Content-Type', 'application/javascript');
                return reply.send(result);
            }
            catch (e) {
                /* handle error */
                console.error(e);
                return;
            }
        }
    }));
}));
// to set the eventsource
const eventSourcePlugin = fastify_plugin_1.default((instance) => __awaiter(void 0, void 0, void 0, function* () {
    // register esCoons for connect
    const esConnections = [];
    instance.decorate('esConnections', esConnections);
    // eventsource
    instance.get(__ES_PATH__, (request, reply) => __awaiter(void 0, void 0, void 0, function* () {
        console.log(`enter eventsource:${request.id}`);
        reply.raw.writeHead(200, { 'Content-Type': 'text/event-stream' });
        esConnections.push(reply.raw);
    }));
    instance.addHook('onClose', (server) => __awaiter(void 0, void 0, void 0, function* () {
        console.log('onClose');
        console.log(server);
        console.log(instance.esConnections);
    }));
    instance.addHook('onTimeout', (request, reply) => __awaiter(void 0, void 0, void 0, function* () {
        console.log('onTimeout');
        console.log(instance.esConnections);
        return;
    }));
}));
const serverIndex = fastify_plugin_1.default((instance, options) => __awaiter(void 0, void 0, void 0, function* () {
    let html = '', clientScript = '';
    try {
        const readResult = yield fsPromises.readFile(path_1.default.join(__dirname, './client.js'), { encoding: 'utf-8' });
        clientScript = readResult.replace('__ES_PATH__', __ES_PATH__);
    }
    catch (e) {
        /* handle error */
        console.error(e);
        console.error('cannot read file for client, hot module reload and css load will be disabled');
    }
    try {
        html = yield fsPromises.readFile(path_1.default.resolve(options.root, 'index.html'), { encoding: 'utf-8' });
        html = html.replace(/<head>/i, '<head><script src="/tserve/client.js" type="module"></script>');
        // inject script
    }
    catch (e) {
        /* handle error */
        console.error('no index.html here, use default html template');
        console.error(e);
        html = `
<!DOCTYPE html>
<html>
  <head>
    <script type="module" src="/tserve/client.js" charset="utf-8"></script>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>App</title>
  </head>
  <body>
    body
  </body>
</html>
    `;
    }
    instance.get('/tserve/client.js', (request, reply) => __awaiter(void 0, void 0, void 0, function* () {
        reply.type('application/javascript');
        reply.send(clientScript);
    }));
    // if no match, return index.html
    instance.get('/', (request, reply) => __awaiter(void 0, void 0, void 0, function* () {
        reply.header('Content-Type', 'text/html');
        return reply.send(html);
    }));
}));
